Skip to content

feat: remove dpr + ui default virtual screen + default screen inset area - #1489

Merged
pravusjif merged 10 commits into
mainfrom
chore/ui-default-vscreen-default-screen-inset
Aug 12, 2026
Merged

feat: remove dpr + ui default virtual screen + default screen inset area#1489
pravusjif merged 10 commits into
mainfrom
chore/ui-default-vscreen-default-screen-inset

Conversation

@pravusjif

@pravusjif pravusjif commented Jul 16, 2026

Copy link
Copy Markdown
Member

Three related UI-renderer changes, all bound to the "Creators Success" signal that UI must be rebuilt/tested separately for desktop and mobile. The virtual-screen half is inherited from the reverted #1444.

1. devicePixelRatio out of UI layout

The UI scale factor is now the contain-fit of the design resolution inside the canvas, and nothing else:

Math.min(canvasWidth / virtualWidth, canvasHeight / virtualHeight)

scaleOnDim drops it too, so 1vw is 1% of the canvas width and 1vh 1% of its height, exactly as in CSS. devicePixelRatio stays on UiCanvasInformation and on ScaleContext as an informational density hint — the "pick a 1x/2x/3x asset" signal — which is the same role it has in CSS and React Native, where it is exposed but never enters layout.

2. Default virtual screen

How setUiRenderer / addUiRenderer resolve the virtual screen used for UI scaling:

  • No virtual size provided → platform default: 1600x720 on mobile, 1920x1080 otherwise. Previously no scaling was applied at all.
  • Invalid size provided (values ≤ 0) → virtual screen disabled: no UI scaling, and any previously applied scale factor is released (matches the old no-options behavior).
  • Valid 16:9 size on mobile (1920x1080, 1280x720, …) → overridden to 1600x720, since phone screens are much wider than 16:9 and a 16:9 canvas would letterbox the UI. A console.log notifies the creator once per provided size, not every tick.
  • Non-16:9 sizes on mobile and any valid size on desktop/web are respected as-is.

The virtual size stays a single scene-wide value: main renderer options win, else the first additional renderer that passed dimensions, else the platform default. Options carrying no dimensions (e.g. only a screenInset) are skipped, so they don't read as a provided-but-invalid size. Defaults apply to scenes that only ever call addUiRenderer() too.

Resolution runs every tick in UiScaleSystem, so it reacts correctly when async platform detection resolves to mobile a few frames after scene start. The virtual screen only applies while a renderer is registered; with no UI at all the scale factor is released.

3. Default screen inset area

Adds an optional screenInset to UiRendererOptions selecting the screen area a renderer's UI is positioned in:

  • 'device' (default) → the device safe area (excludes notch, status bar, rounded corners), from UiCanvasInformation.screenInsetArea. Zero on desktop, so a no-op there.
  • 'interactable' → the area free of the Explorer's native HUD (minimap, chat, …), from UiCanvasInformation.interactableArea.
  • 'none' → the whole screen, 0,0 at its top-left corner; no wrapper entity is added.

'device' is the default because drawing under a notch, a status bar or a rounded corner should be something a creator opts into, not the out-of-the-box behavior. Each renderer honors its own value, so the main UI and additional renderers can use different insets simultaneously.

Inset values are reported by the renderer in canvas pixels, and raw pixel props are multiplied by the UI scale factor when parsed — so ScreenInsetArea / InteractableArea now pre-divide the insets by the scale factor (compensateInsetForUiScale), keeping the values sent to the renderer in canvas pixels at any virtual screen size.

How

  • @dcl/react-ecs cannot depend on ~system/Runtime (and depending on @dcl/sdk would be circular), so a new src/platform.ts exposes an injectable isMobile provider defaulting to non-mobile.
  • @dcl/sdk/react-ecs wires that provider to @dcl/sdk's existing isMobile() helper at module load.
  • system.ts resolves the active virtual size, applies the default / disable / mobile-override rules, and wraps each renderer's component in its selected inset area.

API changes

export type UiRendererOptions = {
  virtualWidth?: number   // now optional
  virtualHeight?: number  // now optional
  screenInset?: UiScreenInset  // defaults to 'device'
}

export type UiScreenInset = 'device' | 'interactable' | 'none'

Versioning

All three changes alter the rendered result of scene code that was not modified — a scene that passed no options gains a virtual screen, UI on mobile gains a device inset, and pixel sizes change on high-density screens. That argues for a minor bump (7.26.0) rather than the patch (7.25.1) that oddish currently computes off the latest tag.

Creator-facing note

A UI already wrapped in <ScreenInsetArea> now sits inside a renderer that is also insetting by default, applying the margin twice. Either drop the wrapper or pass screenInset: 'none'.

Tests

  • virtual-size-defaults.spec.tsx — default / invalid / mobile-override resolution, including addUiRenderer-only scenes.
  • ui-renderer-screen-inset.spec.tsx — per-renderer inset wrapping.
  • interactable-area.spec.tsx / screen-inset-area.spec.tsx — inset scale compensation.
  • virtual-scale-array.spec.tsx, transform.spec.tsx, label.spec.tsx, tree.spec.tsx, background.spec.tsx, button.spec.tsx, add-ui-renderer.spec.tsx — updated for the dpr-free scale factor and the default inset wrapper.
  • ui.ts.crdt snapshot updated: now requires ~system/Runtime for platform detection, adds the inset wrapper entity, and the bundle grows 410.7k → 412.6k.

@pravusjif pravusjif self-assigned this Jul 16, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 16, 2026

Copy link
Copy Markdown

Deploying js-sdk-toolchain with  Cloudflare Pages  Cloudflare Pages

Latest commit: e108df3
Status: ✅  Deploy successful!
Preview URL: https://88f53e48.js-sdk-toolchain.pages.dev
Branch Preview URL: https://chore-ui-default-vscreen-def.js-sdk-toolchain.pages.dev

View logs

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Test this pull request

  • The @dcl/sdk package can be tested in scenes by running

    npm install "https://sdk-team-cdn.decentraland.org/@dcl/js-sdk-toolchain/branch/chore/ui-default-vscreen-default-screen-inset/dcl-sdk-7.25.1-31603173481.commit-655d9dd.tgz"
  • The @dcl/js-runtime package can be tested in scenes by running

    npm install "https://sdk-team-cdn.decentraland.org/@dcl/js-sdk-toolchain/branch/chore/ui-default-vscreen-default-screen-inset/@dcl/js-runtime/dcl-js-runtime-7.25.1-31603173481.commit-655d9dd.tgz"
  • To test with npx init

    export SDK_COMMANDS="https://sdk-team-cdn.decentraland.org/@dcl/js-sdk-toolchain/branch/chore/ui-default-vscreen-default-screen-inset/dcl-sdk-commands-7.25.1-31603173481.commit-655d9dd.tgz"
    npx $SDK_COMMANDS init
  • The /changerealm command to test test in-world

    /changerealm https://sdk-team-cdn.decentraland.org/ipfs/chore/ui-default-vscreen-default-screen-inset-e2e
    
  • You can preview this build entering:
    https://playground.decentraland.org/?sdk-branch=chore/ui-default-vscreen-default-screen-inset

@pravusjif pravusjif moved this to In Progress in Creators Tools Jul 16, 2026
@pravusjif
pravusjif marked this pull request as ready for review August 3, 2026 14:58
@pravusjif pravusjif moved this from In Progress to To Release in Creators Tools Aug 3, 2026
@pravusjif pravusjif moved this from To Release to QA in Creators Tools Aug 3, 2026
`uiScaleFactor` is now exactly the contain-fit of the design resolution
inside the canvas, and `scaleOnDim` resolves 'Nvw'/'Nvh' and string
`fontSize` to N% of the canvas dimension, as in CSS.

devicePixelRatio is a density hint for picking a 1x/2x/3x asset, and each
renderer computes it differently, so dividing by it made UI size inversely
proportional to whichever value the scene happened to get. The field stays
on PBUiCanvasInformation and `ScaleContext.ratio` stays in the public API.
No renderer change is required.

BREAKING CHANGE: scenes that set a virtual size and were calibrated against
the current behaviour will render devicePixelRatio times larger. Scenes that
pass no virtual size keep uiScaleFactor === 1 and are unaffected.

(cherry picked from commit 2995d67)
Drawing under a notch, a status bar or a rounded corner is a bug in every
scene that ships it, so the device safe area is now what a renderer gets when
it doesn't ask for anything. Creators who do want the whole screen opt in with
`screenInset: 'none'`, and `'interactable'` is still there for UI that must
also clear the Explorer's native HUD.

Both setUiRenderer and addUiRenderer default to it, each renderer still
honoring its own value.

Test suites that assert the UI tree hanging directly off the canvas root now
pass the shared WHOLE_SCREEN option, so the wrapper entity stays covered in
ui-renderer-screen-inset.spec.tsx and out of everyone else's assertions.

BREAKING CHANGE: a scene that passes no screenInset now renders inside the
device safe area instead of the whole screen, with one extra wrapper entity
in the UI tree.
@pravusjif pravusjif changed the title chore: ui default virtual screen + default screen inset area chore: remove dpr + ui default virtual screen + default screen inset area Aug 10, 2026
… into chore/ui-default-vscreen-default-screen-inset

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

PR: #1489 — chore: remove dpr + ui default virtual screen + default screen inset area
Branch: chore/ui-default-vscreen-default-screen-insetmain
Files changed: 20 (+1,044 −193)

Three well-motivated changes: removing devicePixelRatio from layout (it was a density hint, not a layout unit), adding default virtual screens (1920×1080 desktop / 1600×720 mobile), and adding per-renderer screenInset with a safe 'device' default. The architecture is clean — the injectable isMobile provider correctly solves the react-ecs~system/Runtime dependency boundary, the symbol-based scale-factor ownership prevents cross-system conflicts, and the per-tick resolution is lightweight (one Map scan + arithmetic, guarded by a change check).

Test coverage is thorough: 572 new lines across virtual-size-defaults.spec.tsx and ui-renderer-screen-inset.spec.tsx, plus updates to 10 existing test files. Edge cases (NaN, zero, dpr=0, late mobile detection, re-wrapping across setUiRenderer calls) are all exercised.

API Surface & Consumer Impact

The TypeScript API changes are backward-compatible: virtualWidth/virtualHeight moved from required to optional, and the new screenInset field is optional. Existing callers compile without changes.

Behavioral changes that affect scenes without code changes:

  • Scenes calling setUiRenderer(ui) with no options now get a default virtual screen (previously: no scaling)
  • All renderers now wrap UI in the device safe area by default
  • High-DPR screens will render UI at a different size (no longer divided by devicePixelRatio)

The PR correctly argues for a minor bump (7.26.0) to signal these behavioral changes. Scenes already wrapping UI in <ScreenInsetArea> will get double-insetting — the PR documents the workaround (screenInset: 'none').

No consumers of UiRendererOptions, setUiRenderer, or addUiRenderer outside js-sdk-toolchain and sdk7-goerli-plaza would break at the TypeScript level. The sdk-skills repo references the old type signature and should be updated when this merges.

Security

No security issues found. No secrets, no injection surfaces, no sensitive data in logs. The setIsMobileProvider export is not reachable from the public @dcl/sdk/react-ecs surface; even if called by a scene, it would only affect that scene's own UI.

Findings

[P2] Stale comment in WHOLE_SCREEN JSDoctest/react-ecs/utils.ts:9
The comment says "The default inset ('interactable')" but the actual default is 'device' (system.ts:75). Three independent reviewers flagged this. Easy fix: s/interactable/device/.

[P2] Providing only one virtual dimension silently disables scalingsystem.ts:77-82
hasVirtualSize({ virtualWidth: 1920 }) returns true (blocks the platform default), but isValidVirtualSize returns false (virtualHeight is undefined → 0 → not > 0), so scaling is disabled entirely with no diagnostic. A one-time console.log warning — matching the mobile-override logging pattern already in place — would help creators catch this misconfiguration.

[P2] wrapWithScreenInset switch lacks exhaustiveness checksystem.ts:209-217
The default branch handles 'none', so adding a fourth UiScreenInset variant in the future would silently fall through. An explicit case 'none': + default: { const _: never = resolvedInset; } is the standard TypeScript exhaustiveness pattern.

[P2] ScaleContext.ratio is now vestigial for layoututils.ts:207-212
The field stays on ScaleContext as a density hint per the comment, and the new test explicitly asserts layout independence from ratio. If no caller currently reads it, consider @deprecated to signal this.

[P2] Deep dist/ import path@dcl/sdk/src/react-ecs.ts:21
import { setIsMobileProvider } from '@dcl/react-ecs/dist/platform' couples to the build output layout. A dedicated exports subpath in package.json (e.g., @dcl/react-ecs/platform) would be cleaner, though dist/ imports may be an established monorepo convention.

Git Conventions (ADR-6)

  • Branch name ✅ chore/<summary>
  • PR title ✅ <type>: <summary> — though feat: might better reflect the behavioral additions (default virtual screen, screenInset option). chore per ADR-6 is "no production code change," and this PR clearly changes production behavior.

Verdict: ✅ APPROVE

No P0 or P1 issues. The P2 findings are minor improvements — the stale comment and the single-dimension edge case are the most actionable. The PR is well-designed, well-documented, and well-tested.


Reviewed by Jarvis 🤖 · Requested by Pravus (<@UDJQDQC0Z>) via Slack

Comment thread test/react-ecs/utils.ts Outdated
Comment thread packages/@dcl/react-ecs/src/system.ts
Comment thread packages/@dcl/react-ecs/src/system.ts
@pravusjif pravusjif changed the title chore: remove dpr + ui default virtual screen + default screen inset area feat: remove dpr + ui default virtual screen + default screen inset area Aug 11, 2026
@pravusjif
pravusjif removed the request for review from nicoecheza August 12, 2026 12:28
@pravusjif
pravusjif merged commit 08dff78 into main Aug 12, 2026
8 checks passed
@pravusjif
pravusjif deleted the chore/ui-default-vscreen-default-screen-inset branch August 12, 2026 14:11
@pravusjif pravusjif moved this from QA to To Release in Creators Tools Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: To Release

Development

Successfully merging this pull request may close these issues.

3 participants